﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Collections;
using System.Net;
using System.Xml;
using System.Threading;

namespace Nextropia.C2Manager.C2API
{
    /// <summary>
    /// management class used for interaction with the C2 API
    /// </summary>
    public class C2Service
    {
        #region public properties
        /// <summary>
        /// returns the current C2 sessionID (if the connected to the C2 server)
        /// </summary>
        public string SessionID { get; private set; }

        /// <summary>
        /// gets or sets the C2 API password 
        /// </summary>
        public string Password { get; set; }

        /// <summary>
        /// gets or sets the C2 API username
        /// </summary>
        public string Username { get; set; }
        
        /// <summary>
        /// returns true if the service is currently connected to the C2 server, false otherwise
        /// </summary>
        public bool IsConnected { get; set; }

#endregion

        #region public methods
        /// <summary>
        /// connects to the C2 server with an existing session
        /// </summary>
        /// <param name="sessionID"></param>
        public void Connect(string sessionID)
        {
            SessionID = sessionID;
            IsConnected = true;
        }

        /// <summary>
        /// connects to the C2 server with a username and password; this creates a new session
        /// </summary>
        /// <param name="username"></param>
        /// <param name="password"></param>
        public void Connect(string username, string password)
        {
            CreateNewSession(username, password);

            Username = username;
            Password = password;

            IsConnected = true;
        }

        /// <summary>
        /// disconnects from the C2 server and destroys the existing session
        /// </summary>
        public void Disconnect()
        {
            try
            {
                ExecuteDataCommand("logout");
            }
            catch {}
                
            IsConnected = false;
            SessionID = null;
        }

        /// <summary>
        /// retrieves price quotes for a list of security symbols
        /// </summary>
        /// <remarks>
        /// price quotes are not guaranteed to be recent and may be delayed
        /// </remarks>
        /// <param name="symbols">a list of C2 security symbols</param>
        //public List<Price> GetQuotes(params string[] symbols)
        //{
        //    XmlDocument doc = ExecuteDataCommand("", new NameValuePair("symbols", String.Join(",", symbols)));

        //    List<Price> prices = new List<Price>();

        //    foreach (XmlNode node in doc.SelectNodes(""))
        //    {
        //        Price price = new Price(node.SelectSingleNode("quote"));
        //        price.Symbol = node.SelectSingleNode("originalSymbol").InnerText;
        //        prices.Add(price);
        //    }

        //    return prices;
        //}

        /// <summary>
        /// returns a list of systems subscribed to by the user with the current C2 API username and password
        /// </summary>
        /// <returns></returns>
        public List<TradingSystem> GetSubscribedSystems()
        {
            XmlDocument doc = ExecuteDataCommand("getsubscriptions");

            List<TradingSystem> systems = new List<TradingSystem>();

            foreach (XmlNode node in doc.SelectNodes("/subscriptions/systemlist/system"))
            {
                systems.Add(new TradingSystem(node));
            }

            return systems;
        }

        /// <summary>
        /// returns a list of systems published by the user with the current C2 API username and password
        /// </summary>
        /// <returns></returns>
        public List<TradingSystem> GetPublishedSystems()
        {
            if (!IsConnected)
                throw new NotConnectedException();

            XmlDocument doc = ExecuteSignalCommand("allsystems");

            List<TradingSystem> systems = new List<TradingSystem>();

            foreach (XmlNode node in doc.SelectNodes("/collective2/systemsowned/system"))
            {
                systems.Add(new TradingSystem(node));
            }

            XmlDocument docSystems = ExecuteSignalCommand("getsystemhypothetical",
                new NameValuePair("systemid", String.Join(".", systems.Select(s => s.ID).ToArray())));

            foreach (XmlNode node in docSystems.SelectNodes("/collective2/hypotheticalEquity/system"))
            {
                TradingSystem system = systems.Single(s => s.ID == node.SelectSingleNode("systemid").InnerText);


                XmlDocument docPositions = ExecuteDataCommand("establishstatus", new NameValuePair("systemid", system.ID));
                XmlNode marginNode = docPositions.SelectSingleNode("/c2xml/status/margin");

                system.Equity = Decimal.Parse(marginNode.SelectSingleNode("equity").InnerText);
                system.MarginUsed = Decimal.Parse(marginNode.SelectSingleNode("marginUsed").InnerText);
                system.BuyingPower = Decimal.Parse(marginNode.SelectSingleNode("availBuyPower").InnerText);
                system.Cash = Decimal.Parse(marginNode.SelectSingleNode("currentCash").InnerText);

                system.Equity += system.Cash;

                foreach (XmlNode nodePosition in docPositions.SelectNodes("/c2xml/status/openPositions/trade"))
                {
                    system.Positions.Add(new Position(system, nodePosition));
                }

                foreach (XmlNode nodeSignal in docPositions.SelectNodes("/c2xml/status/signalsTradedInOpenPositions/signal"))
                {
                    system.ExecutedSignals.Add(new Signal(system, nodeSignal));
                }

                foreach (XmlNode nodeSignal in docPositions.SelectNodes("/c2xml/status/pendingSignals/signal"))
                {
                    system.PendingSignals.Add(new Signal(system, nodeSignal));
                }

                ///HACK: the position info does not include long/short, so we need to look at the first signal 
                ///for each symbol to determine market side

                //foreach (Position position in system.Positions)
                //{
                //    Signal firstSignal = system.ExecutedSignals.OrderBy(s => s.Traded).First(s => s.Symbol == position.Symbol);
                //    if (firstSignal.ActionType == ActionType.BTO)
                //    {
                //        position.QuantityOpened = Math.Abs(position.QuantityOpened);
                //        position.QuantityClosed = Math.Abs(position.QuantityClosed);
                //    }
                //    else
                //    {
                //        position.QuantityOpened = -Math.Abs(position.QuantityOpened);
                //        position.QuantityClosed = -Math.Abs(position.QuantityClosed);
                //    }
                //}
            }

            return systems;
        }

        /// <summary>
        /// utility/shortcut method which returns a list of pending signals for a given system ID
        /// </summary>
        /// <seealso cref="TradingSystem.PendingSignals"/>
        /// <param name="systemID"></param>
        /// <returns></returns>
        public List<Signal> GetPendingSignals(string systemID)
        {
            List<TradingSystem> systems = GetPublishedSystems();

            TradingSystem system = systems.SingleOrDefault(s => s.ID == systemID);

            if (system == null)
                throw new Exception(String.Format("System ID '{0}' not found", systemID));

            return new List<Signal>(system.PendingSignals);
        }

        /// <summary>
        /// utility/shortcut method which returns a list of current positions for a given system ID
        /// </summary>
        /// <seealso cref="TradingSystem.Positions"/>
        /// <param name="systemID"></param>
        /// <returns></returns>
        public List<Position> GetCurrentPositions(string systemID)
        {
            List<TradingSystem> systems = GetPublishedSystems();

            TradingSystem system = systems.SingleOrDefault(s => s.ID == systemID);

            if (system == null)
                throw new Exception(String.Format("System ID '{0}' not found", systemID));

            return new List<Position>(system.Positions);
        }

        /// <summary>
        /// returns a list of recent signals submitted in a system the user is subscribed to
        /// </summary>
        /// <param name="systemID"></param>
        /// <returns></returns>
        public List<Signal> GetSubscribedSignals(string systemID)
        {
            XmlDocument doc = ExecuteDataCommand("getsignalhistory", new NameValuePair("systemid", systemID));

            List<Signal> signals = new List<Signal>();

            TradingSystem system = new TradingSystem();
            system.ID = doc.SelectSingleNode("c2xml/systemid").InnerText;
            system.Name = doc.SelectSingleNode("c2xml/systemname").InnerText;

            foreach (XmlNode node in doc.SelectNodes("/c2xml/signals/signal"))
            {
                signals.Add(new Signal(system, node));
            }

            return signals;
        }

        /// <summary>
        /// returns a list of recent trades (executed signals) submitted in a system the user is subscribed to
        /// </summary>
        /// <param name="systemID"></param>
        /// <returns></returns>
        public List<Trade> GetTrades(string systemID)
        {
            XmlDocument doc = ExecuteDataCommand("gettradehistory", new NameValuePair("systemid", systemID));

            List<Trade> trades = new List<Trade>();

            foreach (XmlNode node in doc.SelectNodes("/c2xml/trades/trade"))
            {
                trades.Add(new Trade(node));
            }

            return trades;
        }

        /// <summary>
        /// creates (submits) an new C2 trading signal
        /// </summary>
        /// <param name="systemID"></param>
        /// <param name="action"></param>
        /// <param name="symbol"></param>
        /// <param name="securityType"></param>
        /// <param name="quantity"></param>
        /// <param name="stop">optional; a value of 0 will be ignored (no stop / market order)</param>
        /// <param name="limit">optional; a value of 0 will be ignored (no limit / market order)</param>
        /// <param name="timeInForce"></param>
        /// <param name="entryTime">time at which the signal should be executed; use a future time to park the signal; use a past time to execute the signal immediatelly</param>
        /// <param name="target">optional; a value of 0 will be ignored (no target); passing a non-zero value here will cause the position to be closed automatically if that value is reached</param>
        /// <param name="stopLoss">optional; a value of 0 will be ignored (no stop loss); passing a non-zero value here will cause the position to be closed automatically if that value is reached</param>
        public void SubmitSignal(string systemID, ActionType action, string symbol, SecurityType securityType, decimal quantity, decimal stop, decimal limit, TimeInForce timeInForce, DateTime entryTime, decimal target, decimal stopLoss)
        {
            SubmitSignal(systemID, action, symbol, securityType, quantity, stop, limit, timeInForce, entryTime, target, stopLoss, null);
        }

        /// <summary>
        /// creates (submits) an new C2 trading signal
        /// </summary>
        /// <param name="systemID"></param>
        /// <param name="action"></param>
        /// <param name="symbol"></param>
        /// <param name="securityType"></param>
        /// <param name="quantity"></param>
        /// <param name="stop">optional; a value of 0 will be ignored (no stop / market order)</param>
        /// <param name="limit">optional; a value of 0 will be ignored (no limit / market order)</param>
        /// <param name="timeInForce"></param>
        /// <param name="entryTime">time at which the signal should be executed; use a future time to park the signal; use a past time to execute the signal immediatelly</param>
        /// <param name="target">optional; a value of 0 will be ignored (no target); passing a non-zero value here will cause the position to be closed automatically if that value is reached</param>
        /// <param name="stopLoss">optional; a value of 0 will be ignored (no stop loss); passing a non-zero value here will cause the position to be closed automatically if that value is reached</param>
        /// <param name="replaceSignalID">optiona; passing a non-null value here will cause the signal with the corresponding ID to be cancelled and replaced with this new one</param>
        public void SubmitSignal(string systemID, ActionType action, string symbol, SecurityType securityType, decimal quantity, decimal stop, decimal limit, TimeInForce timeInForce, DateTime entryTime, decimal target, decimal stopLoss, string replaceSignalID)
        {
            List<NameValuePair> param = new List<NameValuePair>();
            param.Add(new NameValuePair("systemid", systemID));
            param.Add(new NameValuePair("symbol", symbol));
            param.Add(new NameValuePair("instrument", securityType.ToString().ToLower()));
            param.Add(new NameValuePair("quant", quantity));
            param.Add(new NameValuePair("action", action));
            param.Add(new NameValuePair("duration", timeInForce));

            if (stop > 0)
                param.Add(new NameValuePair("stop", stop));

            if (limit > 0)
                param.Add(new NameValuePair("limit", limit));

            if (target > 0)
                param.Add(new NameValuePair("profittarget", target));

            if (stopLoss > 0)
                param.Add(new NameValuePair("stoploss", stopLoss));

            if (replaceSignalID != null)
                param.Add(new NameValuePair("xreplace", replaceSignalID));

            XmlDocument doc = ExecuteSignalCommand("signal", param.ToArray());
        }

        /// <summary>
        /// cancels a previously submitted signal that was not executed yet
        /// </summary>
        /// <param name="systemID"></param>
        /// <param name="signalID"></param>
        public void CancelSignal(string systemID, string signalID)
        {
            XmlDocument doc = ExecuteSignalCommand("cancel",
                new NameValuePair("signalid", signalID),
                new NameValuePair("systemid", systemID)
                );
        }

        /// <summary>
        /// cancels a previously submitted signal that was not executed yet
        /// </summary> 
        public void CancelSignal(Signal signal)
        {
            CancelSignal(signal.System.ID, signal.ID);
        }

        /// <summary>
        /// utility method which attempts to parse a string as a DateTime, and returns DateTime.MinValue if unsuccessful
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public static DateTime SafeParseDate(string input)
        { 
            DateTime date;

            if (!DateTime.TryParse(input, out date))
                date = DateTime.MinValue;

            return date;
        }

        /// <summary>
        /// utility method which attempts to parse a string as a Decimal, and returns 0 if unsuccessful
        /// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public static decimal SafeParseDecimal(string input)
        {
            decimal output;

            if (!decimal.TryParse(input, out output))
                output = 0;

            return output;
        }
#endregion

        #region private properties

        private WebClient webClient = new WebClient();

        #endregion

        # region private methods

        private void CreateNewSession(string username, string password)
        {
            XmlDocument doc = ExecuteDataCommand("getsessionid",
                new NameValuePair("userlogin", username),
                new NameValuePair("pw", password),
                new NameValuePair("clientid", "Nextropia.C2Manager"));

            if (doc.SelectSingleNode("/c2xml/login/valid").InnerText != "1")
                throw new InvalidLoginException();

            SessionID = doc.SelectSingleNode("/c2xml/session/id").InnerText;
        }

        private XmlDocument ExecuteDataCommand(string command, params NameValuePair[] parameters)
        {
            if (command != "getsessionid" && SessionID == null)
                throw new Exception("Not connected");

            if (SessionID != null && !parameters.Any(p => p.Name == "session"))
            {
                var paramList = parameters.ToList();
                paramList.Add(new NameValuePair("session", SessionID));
                parameters = paramList.ToArray();
            }

            //string url = "http://data.collective2.com/cgi-perl/xml.mpl?cmd={0}";

            ////HACK: undocumented quote service has non-standard URL
            //if (command != "getquotes")
            //    url = "http://data.collective2.com/cgi-perl/xml200.mpl?cmd={0}";

            XmlDocument result = BuildExecuteUrl("http://data.collective2.com/cgi-perl/xml.mpl?cmd={0}", command, parameters);

            if (result.SelectSingleNode("/c2xml/error") != null)
            {
                string errorMessage = result.SelectSingleNode("/c2xml/message").InnerText;

                if (errorMessage == "Session expired")
                {
                    CreateNewSession(Username, Password);
                    parameters.First(p => p.Name == "session").Value = SessionID;
                    result = ExecuteDataCommand(command, parameters);
                }
                else
                    throw new Exception(errorMessage);
            }

            return result;
        }

        private XmlDocument ExecuteSignalCommand(string command, params NameValuePair[] parameters)
        {
            var paramList = parameters.ToList();
            paramList.Add(new NameValuePair("pw", Password));
            paramList.Add(new NameValuePair("c2email", Username));
            parameters = paramList.ToArray();

            XmlDocument result = BuildExecuteUrl("http://www.collective2.com/cgi-perl/signal.mpl?cmd={0}", command, parameters);

            XmlNode statusNode = result.SelectSingleNode("/collective2/status");
            if (statusNode != null && statusNode.InnerText == "error")
                if (result.SelectSingleNode("/collective2/errortype") != null)
                    throw new Exception(result.SelectSingleNode("/collective2/errortype").InnerText);
                else if (result.SelectSingleNode("/collective2/errortext") != null)
                    throw new Exception(result.SelectSingleNode("/collective2/errortext").InnerText);
                else
                    throw new Exception(result.SelectSingleNode("/collective2").InnerText);

            return result;
        }

        private XmlDocument BuildExecuteUrl(string baseUrl, string command, NameValuePair[] parameters)
        {
            string url = string.Format(baseUrl, command);

            foreach (NameValuePair parameter in parameters)
                url += "&" + parameter.Name + "=" + parameter.Value;


            string xml = "";
            int failureCount = 3;

            while (failureCount > 0)
            {
                try
                {
                    xml = webClient.DownloadString(url);
                    failureCount = 0;
                }
                catch (WebException)
                {
                    if (failureCount > 0)
                    {
                        failureCount--;
                        Thread.Sleep(5000);
                    }
                    else
                        throw;
                }
            }

            XmlDocument doc = new XmlDocument();

            try
            {
                ///HACK
                //xml = xml.Replace("<BR>", "&lt;BR&gt;");

                doc.LoadXml(xml);
            }
            catch
            {
                throw new Exception("Invalid response received from C2 server: " + xml);
            }

            return doc;
        }

        #endregion
    }
}
